<!--
    Copyright (c) 2017-2021 Julian Knight (Totally Information)

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->

<script type="text/javascript">
/* eslint-env browser es6 */

// Isolate this code
;(function () {
    'use strict'

    /** @typedef {import("node-red").Red} Red */

    //#region --------- "global" variables for the panel --------- //

    /** Module name must match this nodes html file @constant {string} moduleName */
    var moduleName  = 'uibuilder'
    /** Node's label @constant {string} paletteCategory */
    var nodeLabel  = 'uibuilder'
    /** Node's palette category @constant {string} paletteCategory */
    var paletteCategory  = 'uibuilder'
    /** Node's background color @constant {string} paletteColor */
    var paletteColor  = '#E6E0F8'
    /** Default session length (in seconds) if security is active @type {Number} */
    var defaultSessionLength = 432000
    /** Default JWT secret if security is active - to ensure it isn't blank @type {String} */
    var defaultJwtSecret = 'Replace This With A Real Secret'
    /** List of installed packages - rebuilt when editor is opened, updates by library mgr */
    var packages = []
    /** Default template name */
    var defaultTemplate = 'blank'

    /** placeholder for ACE editor vars - so that they survive close/reopen admin config ui
     * @typedef {Object} uiace
     * @property {string} format
     * @property {string} folder
     * @property {string} fname
     * @property {boolean} fullscreen Is the editor in fullscreen mode?
     */
    var uiace = {
        'format': 'html',
        'folder': 'src',
        'fname' : 'index.html',
        'fullscreen': false
    }

    //#endregion ------------------------------------------------- //

    /** Initialise default values for package list - must be done before everything to give the ajax call time to finish
     *  since the list is used to check if the template dependencies are installed.
     * NOTE: This is build dynamically each time the edit panel is opened
     *       we are not saving this since external changes would result in
     *       users having being prompted to deploy even when they've made
     *       no changes themselves to a node instance.
     */
    packageList()

    //#region --------- "global" functions for the panel --------- //

    /** AddItem function for package list
     * @param {JQuery<HTMLElement>} element the jQuery DOM element to which any row content should be added
     * @param {number} index the index of the row
     * @param {string|*} data data object for the row. {} if add button pressed, else data passed to addItem method
     */
    function addPackageRow(element,index,data) {
        var hRow = ''

        if (Object.entries(data).length === 0) {
            // Add button was pressed so we have no packageName, create an input form instead
            hRow='<input type="text" id="packageList-input-' + index + '"> <button id="packageList-button-' + index + '">Install</button>'
        } else {
            // addItem method was called with a packageName passed
            hRow = data
        }
        // Build the output row
        var myRow = $('<div id="packageList-row-' + index + '" class="packageList-row-data">'+hRow+'</div>').appendTo(element)

        // Create a button click listener for the install button for this row
        $('#packageList-button-' + index).click(function(){
            // show activity spinner
            $('i.spinner').show()
            
            var packageName = '' + $('#packageList-input-' + index).val()

            if ( packageName.length !== 0 ) {
                /** @type {Red} */
                RED.notify('Installing npm package ' + packageName)

                // Call the npm installPackage API (it updates the package list)
                $.get( 'uibnpmmanage?cmd=install&package=' + packageName, function(data){

                    if ( data.success === true) {
                        console.log('[uibuilder:addPackageRow:get] PACKAGE INSTALLED. ', packageName)
                        RED.notify('Successful installation of npm package ' + packageName, 'success')

                        // Replace the input field with the normal package name display
                        myRow.html(packageName)

                        // Update the master package list
                        packages.push(packageName)
                    } else {
                        console.log('[uibuilder:addPackageRow:get] ERROR ON INSTALLATION OF PACKAGE ', packageName )
                        console.dir( data.result )
                        RED.notify('FAILED installation of npm package ' + packageName, 'error')
                    }

                    // Hide the progress spinner
                    $('i.spinner').hide()

                })
                .fail(function(_jqXHR, textStatus, errorThrown) {
                    console.error( '[uibuilder:addPackageRow:get] Error ' + textStatus, errorThrown )
                    RED.notify('FAILED installation of npm package ' + packageName, 'error')

                    $('i.spinner').hide()
                    return 'addPackageRow failed'
                    // TODO otherwise highlight input
                })
            } // else Do nothing

        }) // -- end of button click -- //

    } // --- End of addPackageRow() ---- //

    /** RemoveItem function for package list */
    function removePackageRow(packageName) {

        // If package name is an empty object - user removed an add row so ignore
        if ( (packageName === '') || (typeof packageName !== 'string') ) {
            return false
        }

        RED.notify('Starting removal of npm package ' + packageName)
        // show activity spinner
        $('i.spinner').show()

        // Call the npm installPackage API (it updates the package list)
        $.get( 'uibnpmmanage?cmd=remove&package=' + packageName, function(data){

            if ( data.success === true) {
                console.log('[uibuilder:removePackageRow:get] PACKAGE REMOVED. ', packageName)
                RED.notify('Successfully uninstalled npm package ' + packageName, 'success')
                
                // Remove the entry from the master package list
                const i = packages.indexOf(packageName)
                if ( i > 0 ) packages.splice(i,1)
            } else {
                console.log('[uibuilder:removePackageRow:get] ERROR ON PACKAGE REMOVAL ', data.result )
                RED.notify('FAILED to uninstall npm package ' + packageName, 'error')
                // Put the entry back again
                $('#node-input-packageList').editableList('addItem',packageName)
            }

            $('i.spinner').hide()

        })
        .fail(function(_jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:removePackageRow:get] Error ' + textStatus, errorThrown )
            RED.notify('FAILED to uninstall npm package ' + packageName, 'error')
            
            // Put the entry back again
            $('#node-input-packageList').editableList('addItem',packageName)

            $('i.spinner').hide()
            return 'removePackageRow failed'
            // TODO otherwise highlight input
        })
        
    } // ---- End of removePackageRow ---- //

    /** Get list of installed packages via API - save to master list */
    function packageList() {
        $.getJSON('uibvendorpackages', function(vendorPaths) {
            packages = []
            var pkgList = Object.keys(vendorPaths)
            pkgList.forEach(function(packageName,_index){
                // Populate the master package list (used to check dependencies)
                packages.push(packageName)
            })
        })
    } // --- End of packageList --- //

    /** Return a file type from a file name (or default to txt)
     *  ftype can be used in ACE editor modes */
    function fileType(fname) {
        let ftype = 'text'
        let fparts = fname.split('.')
        // Take off the first entry if the file name started with a dot
        if ( fparts[0] === '' ) fparts.shift()
        if (fparts.length > 1) {
            // Get the last element of the array
            let fext = fparts.pop().toLowerCase().trim()
            switch (fext) {
                case 'js':
                    ftype = 'javascript'
                    break
                case 'html':
                case 'css':
                case 'json':
                    ftype = fext
                    break
                case 'vue':
                    ftype = 'html'
                    break
                case 'md':
                    ftype = 'markdown'
                    break
                case 'yaml':
                case 'yml':
                    ftype = 'yaml'
                    break
                default:
                    ftype = fext
            }
        }
        return ftype
    } // --- End of fileType --- //

    /** Get the list of files for the chosen url & folder
     * @param {string} [selectedFile] Optional. If present will select this filename after refresh, otherwise 1st file is selected.
     */
    function getFileList(selectedFile) {
        //#region --- Collect variables from Config UI ---
        var url = $('#node-input-url').val()
        var folder = $('#node-input-folder').val()
        var f = $('#node-input-filename').val()
        
        // Whether or not to force the index.(html|js|css) files to be copied over if missing
        var nodeInputCopyIndex = $('#node-input-copyIndex').is(':checked')
        //#endregion -------------------------------------

        // Collect the current filename from various places
        if ( selectedFile === undefined ) selectedFile = f
        if ( selectedFile === null ) 
            selectedFile = localStorage.getItem('uibuilder.'+url+'.selectedFile') || undefined

        if ( folder === null ) 
            folder = localStorage.getItem('uibuilder.'+url+'.folder') || undefined

        // Clear out drop-downs ready for rebuilding
        $('#node-input-filename option').remove()
        $('#node-input-folder option').remove()

        // Get all files/folders for this uibuilder instance from API call
        $.ajax({
            type: 'GET',
            dataType: 'json',
            url: './uibuilder/admin/' + url,
            data: {
                'cmd': 'listall',
            },
        })
            .done(function(data, textStatus, jqXHR) {
                let firstFile = '', indexHtml = false, selected = false

                // build folder list, pre-select src if no current folder selected #node-input-folder - Object.keys(res)
                const folders = Object.keys(data).sort()

                // Rebuild folder drop-down
                $.each(folders, function (i, fldrname) {
                    // For the root folder, use empty string for folders lookup but "root" for display
                    if ( fldrname === '' ) fldrname = 'root'
                    // Build the drop-down
                    $('#node-input-folder').append($('<option>', { 
                        value: fldrname,
                        text : fldrname, 
                    }))
                })
                // if currently selected folder doesn't exist
                if ( !data[folder] ) {
                    // Use 'src' if it exists otherwise use 'root'
                    if ( data['src'] ) folder = 'src'
                    else folder = root
                }
                // Selected folder
                $('#node-input-folder').val(folder)
                uiace.folder = folder
                // Persist the folder selection
                localStorage.setItem('uibuilder.'+url+'.folder', folder)

                let files = []
                files = data[folder]
                
                $.each(files, function (i, filename) {
                    // Build the drop-down
                    $('#node-input-filename').append($('<option>', { 
                        value: filename,
                        text : filename, 
                    }))
                    // Choose the default file. In order: selectedFile param, index.html, 1st returned
                    if ( i === 0 ) firstFile = filename
                    if ( filename === 'index.html' ) indexHtml = true
                    if ( filename === selectedFile ) selected = true
                })
                // Set default file name/type. In order: selectedFile param, index.html, 1st returned
                if ( selected === true ) uiace.fname = selectedFile
                else if ( indexHtml === true ) uiace.fname = 'index.html'
                else uiace.fname = firstFile
                $('#node-input-filename').val(uiace.fname)
                uiace.format = fileType(uiace.fname)
                // Persist the file name selection
                localStorage.setItem('uibuilder.'+url+'.selectedFile', uiace.fname)

            })
            .fail(function(jqXHR, textStatus, errorThrown) {

                console.error( '[uibuilder:getFileList:getJSON] Error ' + textStatus, errorThrown )
                uiace.fname = ''
                uiace.format = 'text'
                RED.notify(`uibuilder: Folder and file listing error.<br>${errorThrown}`, {type:'error'})

            })
            .always(function() {
            getFileContents()
        })

    } // --- End of getFileList --- //

    /** Get the chosen file contents & set up the ACE editor */
    function getFileContents() {
        /** Get the chosen folder name - use the default/last saved on first load 
         * @type {string} */ 
         // @ts-ignore
        var folder = $('#node-input-folder').val()
        if ( folder === null ) 
            folder = localStorage.getItem('uibuilder.'+url+'.folder') || uiace.folder 
        /** Get the chosen filename - use the default/last saved on first load 
         * @type {string} */
         // @ts-ignore
        var fname = $('#node-input-filename').val()
        if ( fname === null ) 
            fname = localStorage.getItem('uibuilder.'+url+'.selectedFile') || uiace.fname 
        // Get the current url
        var url = $('#node-input-url').val()

        // Save the file & folder names
        uiace.folder = folder
        uiace.fname = fname

        // Persist the folder & file name selection
        localStorage.setItem('uibuilder.'+url+'.folder', uiace.folder)
        localStorage.setItem('uibuilder.'+url+'.selectedFile', uiace.fname)

        // Change mode to match file type
        var filetype = uiace.format = fileType(fname)
        $('#node-input-format').val(filetype)

        // Get the file contents via API defined in uibuilder.js
        $.get( 'uibgetfile?url=' + url + '&fname=' + fname + '&folder=' + folder, function(data){
            $('#node-input-template-editor').show()
            $('#node-input-template-editor-no-file').hide()
            // Add the fetched data to the editor
            uiace.editorSession.setValue(data)
            // Set the editor file mode
            uiace.editorSession.setMode({
                path: 'ace/mode/' + filetype, v: Date.now()
            })
            // Mark the current session as clean
            uiace.editorSession.getUndoManager().isClean()
            // Position the cursor in the edit area
            uiace.editor.focus()

        })
        .fail(function(_jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:getFileContents:get] Error ' + textStatus, errorThrown )
            uiace.editorSession.setValue('')
            $('#node-input-template-editor').hide()
            $('#node-input-template-editor-no-file').show()

        })
        .always(function(){
            fileIsClean(true)
            // Default the language selector in case it wasn't recognised
            if(!$('#node-input-format option:selected').length) $('#node-input-format').val('text')
        })
    } // --- End of getFileContents --- //

    /** Call v3 admin API to create a new folder 
     * @param {string} folder Name of new folder to create (combines with current uibuilder url)
     * @return {string} Status message
     * 
     */
    function createNewFolder(folder) {
        // Also get the current url
        var url = $('#node-input-url').val()

        $.ajax({
            type: 'POST',
            dataType: 'json',
            url: './uibuilder/admin/' + url,
            data: {
                'folder': folder,
                'cmd': 'newfolder',
            },
        })
        .done(function(data, textStatus, jqXHR) {
            RED.notify(`uibuilder: Folder <code>${folder}</code> Created.`, {type:'success'})
            // Rebuild the file list
            getFileList()

            return 'Create folder succeeded'
        }).fail(function(jqXHR, textStatus, errorThrown) {
            RED.notify(`uibuilder: Create Folder Error.<br>${errorThrown}`, {type:'error'})
            return 'Create folder failed'
        })
    } // --- End of createNewFile --- //

    /** Call v3 admin API to create a new file 
     * @param {string} fname Name of new file to create (combines with current selected folder and the current uibuilder url)
     * @return {string} Status message
     * 
     */
    function createNewFile(fname) {
        // Also get the current folder & url
        var folder = $('#node-input-folder').val() || uiace.folder
        var url = $('#node-input-url').val()

        $.ajax({
            type: 'POST',
            dataType: 'json',
            url: './uibuilder/admin/' + url,
            data: {
                'folder': folder,
                'fname': fname,
                'cmd': 'newfile',
            },
        })
        .done(function(data, textStatus, jqXHR) {
            RED.notify(`uibuilder: File <code>${folder}/${fname}</code> Created.`, {type:'success'})
            // Rebuild the file list
            getFileList(fname)

            return 'Create file succeeded'
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:createNewFile:post] Error ' + textStatus, errorThrown )
            RED.notify(`uibuilder: Create File Error.<br>${errorThrown}`, {type:'error'})
            return 'Create file failed'
        })
    } // --- End of createNewFile --- //

    /** Call v3 admin API to delete the currently selected folder 
     * @return {string} Status message
     */
    function deleteFolder() {
        // Also get the current url & folder
        var url = $('#node-input-url').val()
        var folder = $('#node-input-folder').val()

        $.ajax({
            type: 'DELETE',
            dataType: 'json',
            url: './uibuilder/admin/' + url,
            data: {
                'folder': folder,
                'cmd': 'deletefolder',
            },
        })
        .done(function(data, textStatus, jqXHR) {
            RED.notify(`uibuilder: Folder <code>${folder}</code> deleted.`, {type:'success'})
            // Rebuild the file list
            getFileList()

            return 'Delete folder succeeded'
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:deleteFolder:delete] Error ' + textStatus, errorThrown )
            RED.notify(`uibuilder: Delete Folder Error.<br>${errorThrown}`, {type:'error'})
            return 'Delete folder failed'
        })

    } // --- End of deleteFolder --- //

    /** Call v3 admin API to delete the currently selected file 
     * @return {string} Status message
     */
    function deleteFile() {
        // Get the current file, folder & url
        var folder = $('#node-input-folder').val() || uiace.folder
        var url = $('#node-input-url').val()
        var fname = $('#node-input-filename').val()

        $.ajax({
            type: 'DELETE',
            dataType: 'json',
            url: './uibuilder/admin/' + url,
            data: {
                'folder': folder,
                'fname': fname,
                'cmd': 'deletefile',
            },
        })
        .done(function(data, textStatus, jqXHR) {
            RED.notify(`uibuilder: File <code>${folder}/${fname}</code> Deleted.`, {type:'success'})
            // Rebuild the file list
            getFileList(fname)

            return 'Delete file succeeded'
        }).fail(function(jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:deleteFile:delete] Error ' + textStatus, errorThrown )
            RED.notify(`uibuilder: Delete File Error.<br>${errorThrown}`, {type:'error'})
            return 'Delete file failed'
        })

    } // --- End of deleteFile --- //

    /** Enable/disable buttons if file has edits or not
     * @param {boolean} isClean true = the file is clean, else there are pending edits that need saving
     */
    function fileIsClean(isClean) {
        // If clean, disable the save & reset buttons
        $('#edit-save').prop('disabled', isClean)
        $('#edit-reset').prop('disabled', isClean)
        // If clean, enable the delete and edit buttons
        //$('#edit-delete').prop('disabled', !isClean)
        $('#edit-close').prop('disabled', !isClean)
        $('#node-edit-file').prop('disabled', !isClean)
        $('#node-input-filename').prop('disabled', !isClean)
        // If not clean, disable main Done and Cancel buttons to prevent loss
        $('#node-dialog-ok').prop('disabled', !isClean)
        $('#node-dialog-cancel').prop('disabled', !isClean)
        // If not clean, Add a user hint
        if ( ! isClean ) {
            $('#file-action-message').text('Save Required')
            $('#node-dialog-ok').css( 'cursor', 'not-allowed' )
            $('#node-dialog-cancel').css( 'cursor', 'not-allowed' )
        } else {
            $('#node-dialog-ok').css( 'cursor', 'pointer' )
            $('#node-dialog-cancel').css( 'cursor', 'pointer' )
        }
    } // --- End of fileIsClean --- //

    /** Validation Function: Validate the url property
     * Max 20 chars, can't contain any of ['..', ]
     * @param {*} value The url value to validate
     * @returns {boolean} true = valid
     **/
    function validateUrl(value) {
        // NB: `this` is the node instance configuration as at last press of Done
        //     Validation fns are run on every node instance when the Editor is loaded (value is populated but jQuery val is undefined).
        //     and again when the config panel is opened (because jquery change is fired. value, jquery val and this.url are all the same).

        /** If the url hasn't really changed - no need to validate but might need to allow Done (for other settings)
         * e.g. user changes is back after trying something different but didn't commit between
         * Initial flow run and Initial load of admin config ui - don't check for dups as it always will be a dup
         */
        if ( value === this.url ) {
            $('#node-dialog-ok').prop('disabled', false)
            $('#node-dialog-ok').css( 'cursor', 'pointer' )
            $('#node-dialog-ok').addClass('primary')
            return true
        }

        // Max 20 chars
        if ( value.length > 20 ) return false
        // Cannot contain ..
        if ( value.indexOf('..') !== -1 ) return false
        // cannot contain / or \
        if ( value.indexOf('/') !== -1 ) return false
        if ( value.indexOf('\\') !== -1 ) return false
        // Cannot start with _ or .
        if ( value.substring(0,1) === '_' ) return false
        if ( value.substring(0,1) === '.' ) return false
        // Cannot be 'templates' as this is a reserved value (for v2)
        if ( value.toLowerCase().substring(0,9) === 'templates' ) return false

        
        // Check whether the url is already in use via a call to the admin API
        var exists = false
        $.ajax({
            type: 'GET',
            async: false,
            dataType: 'json',
            url: './uibuilder/admin/' + value,
            data: {
                'cmd': 'checkurls',
            },
            success: function(check) {
                exists = check
            }
        })

        /** If the url already exists - prevent the "Done" button from being pressed. */
        if ( exists === true ) {
            $('#node-dialog-ok').prop('disabled', true)
            $('#node-dialog-ok').css( 'cursor', 'not-allowed' )
            $('#node-dialog-ok').removeClass('primary')
            RED.notify(`<b>ERROR</b>: <p>The chosen URL (${value}) is already in use (or the folder exists).<br>It must be changed before you can save/commit</p>`, {type: 'error'})
            return false
        } else {
            $('#node-dialog-ok').prop('disabled', false)
            $('#node-dialog-ok').css( 'cursor', 'pointer' )
            $('#node-dialog-ok').addClass('primary')

            // Warn user when changing URL. NOTE: Set/reset old url in the onsave function not here
            if ( value !== this.url )
                RED.notify(`<b>NOTE</b>: <p>You are renaming the url from ${this.url} to ${value}.<br>You <b>MUST</b> redeploy before doing anything else.</p>`, {type: 'warning'})

            return true
        }

    } // --- End of validateUrl --- //

    /** Set the height of the ACE text editor box */
    function setACEheight() {
        var height

        if ( uiace.editorLoaded === true ) {
            // If the editor is in full-screen ...
            if (document.fullscreenElement) {
                // Force background color and add some padding to keep away from edge
                $('#edit-props').css('background-color','#f6f6f6').css('padding','1em')

                // Start to calculate the available height and adjust the editor to fill the ht
                height = parseInt($('#edit-props').css('height')) // full available height
                height -= 25

                // Replace the expand icon with a compress icon
                $('#node-function-expand-js').css('background-color','black').html('<i class="fa fa-compress"></i>')

                uiace.fullscreen = true

            } else {
                $('#edit-props').css('background-color','').css('padding','')

                // Full height
                height = parseInt($('#dialog-form').height())
                // Subtract info lines
                height -= parseInt($('#info').height())
                
                // Replace the compress icon with a expand icon
                $('#node-function-expand-js').css('background-color','').html('<i class="fa fa-expand"></i>')

                uiace.fullscreen = false
                
            }
            
            // everything but the edit box
            var rows = $('#edit-props > div:not(.node-text-editor-row)')

            // subtract height of each row from the total
            for (var i=0; i<rows.length; i++) {
                height -= $(rows[i]).outerHeight(true)
            }

            // Set the height of the edit box
            $('#node-input-template-editor').css('height',height+'px')

            // Get the content to match the edit box size
            uiace.editor.resize()

        }

    } // --- End of setACEheight --- //

    /** Validation Function: Validate the session length property
     * If security is on, must contain a number
     * @param {Number} value The session length value to validate
     * @returns {boolean} true = valid, false = not valid
     **/
    function validateSessLen(value) {
        // NB: `this` is the node instance configuration as at last press of Done
        // TODO: Add display comment to help user

        var newVal = $('#node-input-sessionLength').val()
        var newSec = $('#node-input-useSecurity').is(':checked')

        if (newSec === true) {
            if ( newVal.length < 1 ) return false
        }

        return true

    } // --- End of validateSessLen --- //

    /** Validation Function: Validate the jwtSecret property
     * If security is on, must contain text
     * @param {String} value The jwt secret value to validate
     * @returns {boolean} true = valid, false = not valid
     **/
    function validateSecret(value) {
        // NB: `this` is the node instance configuration as at last press of Done
        // TODO: Add display comment to help user

        var newVal = $('#node-input-jwtSecret').val()
        var newSec = $('#node-input-useSecurity').is(':checked')

        if (newSec === true) {
            if ( newVal.length < 1 ) return false
        }

        return true

    } // --- End of validateSecret --- //

    /** Populate the template selection dropdown
     * Uses a file that is `require`d in uibuilder.js
    */
    function populateTemplateDropdown(that) {

        let templateFolder = that.templateFolder

        // Load each option - first entry will be the default
        Object.values(RED.settings.uibuilderTemplates).forEach( templ => {
            // Build the drop-down
            $('#node-input-templateFolder').append($('<option>', { 
                value: templ.folder,
                text : templ.name, 
            }))
        })

        // Just in case name doesn't exist in template list, default to blank
        if ( ! RED.settings.uibuilderTemplates[templateFolder] ) {
            templateFolder = defaultTemplate
        }

        $(`#node-input-templateFolder option[value="${templateFolder}"]`).prop('selected', true)
        $('#node-input-templateFolder').val(templateFolder)

        // 1st entry is default - populate the description help tip
        $('#node-templSel-info').text(RED.settings.uibuilderTemplates[templateFolder].description)

        return templateFolder
    } // --- end of function populateTemplateDropdown --- //

    /** Check whether the currently selected template's npm dependencies are installed
     * If not, warn the user to install them.
     */
    function checkDependencies() {
        const folder = $('#node-input-templateFolder').val()

        if ( ! RED.settings.uibuilderTemplates[folder] ) {
            console.error(`[uibuilder:checkDependencies] Template name not found: ${folder}`)
            return
        }

        const deps = RED.settings.uibuilderTemplates[folder].dependencies || []
        const missing = []

        deps.forEach( depName => {
            if ( ! packages.includes(depName) ) missing.push(depName)
        })

        if ( missing.length > 0 ) {
            var myNotification = RED.notify(`WARNING<br /><br />The selected uibuilder template (${folder}) is MISSING the following dependencies:<div> ${missing.join(', ')}</div><br />Please install them using the uibuilder Library Manager or select a different template.`,{
                type: 'warning',
                modal: true,
                fixed: true,
                buttons: [
                    {
                        text: "OK",
                        class:"primary",
                        click: function(e) {
                            myNotification.close();
                        }
                    }
                ]
            })
        }

    } // --- end of function checkDependencies --- //

    /** Load a new template */
    function btnTemplate() {

        // Check first
        var myNotification = RED.notify(
            `WARNING<br /><br />
            Are you <b>SURE</b> you want to overwrite your code
            with the template <b><code>${ $('#node-input-templateFolder').val() }</code></b>?<br /><br />
            <b>THIS CANNOT BE UNDONE.</b>`,
            {
                type: 'warning',
                modal: true,
                fixed: true,
                buttons: [
                    {
                        text: "Cancel, don't overwrite",
                        class:"primary",
                        click: function(e) {
                            myNotification.close()
                        }
                    },
                    {
                        text: "OK, overwrite",
                        //class:"primary",
                        click: function(e) {
                            loadTemplate()
                            myNotification.close()
                        }
                    }
                ]
            }
        )

    } // --- End of btnTemplate() --- //
    function loadTemplate() {
        // Get the current file, folder & url
        var template = $('#node-input-templateFolder').val()
        var extTemplate = $('#node-input-extTemplate').val()
        var url = $('#node-input-url').val()

        $.ajax({
            type: 'POST',
            dataType: 'json',
            url: './uibuilder/admin/' + url,
            data: {
                'template': template,
                'extTemplate': extTemplate,
                'cmd': 'replaceTemplate',
            },
        })
        .done(function(data, textStatus, jqXHR) {
            RED.notify(`<b>uibuilder</b>:<br><br>${jqXHR.statusText}<br>`, {type:'success'})
            return 'Template load succeeded'
        })
        .fail(function(jqXHR, textStatus, errorThrown) {
            console.error( '[uibuilder:loadTemplate] Error ' + textStatus, errorThrown )
            RED.notify(`<b>uibuilder</b>:<br><br>Template Load Error.<br><br>${errorThrown}<br>`, {type:'error'})
            return 'Template load failed'
        })
    } // --- End of loadTemplate() --- //

    //#endregion ------------------------------------------------- //

    // Register the node type, defaults and set up the edit fns 
    RED.nodes.registerType(moduleName, {
        category: paletteCategory,
        color: paletteColor,
        defaults: {
            name: { value: '' },
            topic: { value: '' },
            url: { value: moduleName, required: true, validate: validateUrl },
            fwdInMessages: { value: false },   // Should we send input msg's direct to output as well as the front-end?
            allowScripts: { value: false },    // Should we allow msg's to send JavaScript to the front-end?
            allowStyles: { value: false },     // Should we allow msg's to send CSS styles to the front-end?
            copyIndex: { value: true },        // DEPRECATED Should the default template files be copied to the instance src folder?
            templateFolder: { value: defaultTemplate },  // Folder for selected template
            extTemplate: { value: '' }, // Only if templateFolder=external, degit name
            showfolder: { value: false },      // Should a web index view of all source files be made available?
            useSecurity: { value: false },
            sessionLength: { value: defaultSessionLength, validate: validateSessLen },   // 5d - Must have content if useSecurity=true
            tokenAutoExtend: { value: false }, // TODO add validation if useSecurity=true
            oldUrl: { value: undefined },      // If the url has been changed, this is the previous url
            reload: { value: false },          // If true, all connected clients will be reloaded if a file is changed on the edit screens
            sourceFolder: { value: 'src', required: true, }, // Which folder to use for front-end code? (src or dist)
            //jwtSecret: { value: defaultJwtSecret, validate: validateSecret },   // Must have content if useSecurity=true
        },
        credentials: {
            jwtSecret: { type:'password' },  // text or password
        },
        inputs: 1,
        inputLabels: 'Msg to send to front-end',
        outputs: 2,
        outputLabels: ['Data from front-end', 'Control Msgs from front-end'],
        icon: 'ui_template.png',
        paletteLabel: nodeLabel,
        label: function () { return this.url || this.name || nodeLabel },

        /** Available methods: 
         * oneditprepare: (function) called when the edit dialog is being built.
         * oneditsave:   (function) called when the edit dialog is okayed.
         * oneditcancel: (function) called when the edit dialog is canceled.
         * oneditdelete: (function) called when the delete button in a configuration node’s edit dialog is pressed.
         * oneditresize: (function) called when the edit dialog is resized.
         * onpaletteadd: (function) called when the node type is added to the palette.
         * onpaletteremove: (function) called when the node type is removed from the palette.
         */

        /** Prepares the Editor panel */
        oneditprepare: function () {
            //console.log('[uibuilder:oneditprepare] THIS: ', this)

            var that = this

            // Bug fix for messed up recording of template up to uib v3.3, fixed in v4
            if ( that.templateFolder === undefined || that.templateFolder === '' ) that.templateFolder = defaultTemplate

            //#region Start with the edit section hidden & main section visible
            $('#main-props').show()
            $('#edit-props').hide()
            $('#npm-props').hide()
            $('#adv-props').hide()
            $('#sec-props').hide()
            $('#info-props').hide()
            //#endregion

            //#region Set the checkbox states
            $('#node-input-fwdInMessages').prop('checked', this.fwdInMessages)
            $('#node-input-allowScripts').prop('checked', this.allowScripts)
            $('#node-input-allowStyles').prop('checked', this.allowStyles)
            $('#node-input-copyIndex').prop('checked', this.copyIndex)
            $('#node-input-showfolder').prop('checked', this.showfolder)
            $('#node-input-useSecurity').prop('checked', this.useSecurity)
            $('#node-input-tokenAutoExtend').prop('checked', this.useSecurity)
            $('#node-input-reload').prop('checked', this.reload)
            //#endregion checkbox states

            // Set sourceFolder dropdown
            $(`#node-input-sourceFolder option[value="${that.sourceFolder || 'src'}"]`).prop('selected', true)
            $('#node-input-sourceFolder').val(that.sourceFolder || 'src')

            /** When the url changes (NB: Also see the validation function) change visible folder names & links
             * NB: Actual URL change processing is done in validation which also happens on change
             *     Change happens when config panel is opened as well as for a real change
             */
            $('#node-input-url').change(function () {
                var thisurl = $(this).val()
                var eUrlSplit = window.origin.split(':')
                var nrPort = Number(eUrlSplit[2])
                var nodeRoot = RED.settings.httpNodeRoot.replace(/^\//, '')
                $('#info-webserver').empty()
                // Is uibuilder using a custom server?
                if (RED.settings.uibuilderCustomServer.port) {
                    // Use the correct protocol (http or https)
                    eUrlSplit[0] = RED.settings.uibuilderCustomServer.type.replace('http2','https')
                    // Use the correct port
                    eUrlSplit[2] = RED.settings.uibuilderCustomServer.port
                    // When using custom server, no base path is used
                    nodeRoot = ''
                    $('#info-webserver')
                        .append(`<div class="form-tips node-help">uibuilder is using a custom webserver at ${eUrlSplit.join(':') + '/'} </div>`)
                }
                var urlPrefix = eUrlSplit.join(':') + '/'
                // Show the root URL
                $('#uibuilderurl').prop('href', urlPrefix + nodeRoot + thisurl).text('Open ' + nodeRoot + thisurl)
                $('#node-input-showfolder-url').empty().append('<a href="' + urlPrefix + nodeRoot + $(this).val() + '/idx" target="_blank">' + nodeRoot + $(this).val() + '/idx</a>')
                $('#uibinstanceconf').prop('href', `./uibuilder/instance/${thisurl}?cmd=showinstancesettings`)
            })

            //#region ---- Template settings ---- //

            $('#adv-templ').hide()
            $('#show-templ-props').css( 'cursor', 'pointer' )
            $('#show-templ-props').click(function(_e) {
                $('#adv-templ').toggle()
                if ( $('#adv-templ').is(':visible') ) {
                    $('#show-templ-props').html('<i class="fa fa-caret-down"></i> Template Settings')
                } else {
                    $('#show-templ-props').html('<i class="fa fa-caret-right"></i> Template Settings')
                }
            })
            // Populate the template selection drop-down and select default (in advanced)
            populateTemplateDropdown(that)
            checkDependencies()
            // Unhide the external template name input if external selected
            if ( $('#node-input-templateFolder').val() === 'external' ) $('#et-input').show()
            // Handle change of template
            $('#node-input-templateFolder').change(function(_e) {
                // update the help tip
                if ( RED.settings.uibuilderTemplates[that.templateFolder] )
                    $('#node-templSel-info').text(RED.settings.uibuilderTemplates[that.templateFolder].description)
                // Check if the dependencies are installed, warn if not
                checkDependencies()
                // Unhide the external template name input if external selected
                if ( $('#node-input-templateFolder').val() === 'external' )
                    $('#et-input').show()
                else
                    $('#et-input').hide()
            })
            // Button press for loading new template
            $('#btn-load-template').click(function(e){
                e.preventDefault() // don't trigger normal click event
                btnTemplate()
            })
            
            //#endregion ---- ---- //

            //#region ---- security settings ---- //
            
            // Show/Hide the security settings
            $('#show-security-props').css( 'cursor', 'pointer' )
            $('#show-security-props').click(function(_e) {
                $('#sec-props').toggle()
                if ( $('#sec-props').is(':visible') ) {
                    $('#show-security-props').html('<i class="fa fa-caret-down"></i> Security Settings')
                } else {
                    $('#show-security-props').html('<i class="fa fa-caret-right"></i> Security Settings')
                }
            })

            // One-off check for default settings
            if ( $('#node-input-jwtSecret').val().length === 0 ) {
                $('#node-input-jwtSecret').val(defaultJwtSecret)
            }
            if ( $('#node-input-useSecurity').is(':checked') ) {
                if ( $('#node-input-sessionLength').val().length === 0 ) {
                    $('#node-input-sessionLength').val(defaultSessionLength)
                }
            }

            // Security turning on/off
            $('#node-input-useSecurity').change(function() {
                // security is requested, enable other settings and add warnings if needed
                if ( this.checked ) {
                    // If in production, cannot turn on security without https, in dev, give a warning
                    if (window.location.protocol !== 'https' && window.location.protocol !== 'https:') {
                        if (RED.settings.uibuilderNodeEnv !== 'development') {
                            console.error('HTTPS NOT IN USE BUT SECURITY REQUESTED AND Node environment is NOT "development"')
                            $('#node-input-useSecurity').prop('checked', false); this.checked = false
                        } else {
                            console.warn('HTTPS NOT IN USE BUT SECURITY REQUESTED - Node environment is "development" so this is allowed but not recommended')
                        }
                        // TODO: Add user warnings
                    }
                }
                // Yes, we do need this.checked twice :-)
                if ( this.checked ) {
                    $('#node-input-sessionLength').prop('disabled', false)
                    $('#node-input-jwtSecret').prop('disabled', false)
                    $('#node-input-tokenAutoExtend').prop('disabled', false)
                    // Add defaults if fields are empty
                    if ( $('#node-input-jwtSecret').val().length === 0 ) {
                        $('#node-input-jwtSecret').addClass('input-error')
                    }
                    if ( $('#node-input-sessionLength').val().length === 0 ) {
                        $('#node-input-sessionLength').val(defaultSessionLength)
                    }
                    if ( $('#node-input-jwtSecret').val().length === 0 ) {
                        $('#node-input-jwtSecret').val(defaultJwtSecret)
                    }

                } else { // security not requested, disable other settings
                    $('#node-input-sessionLength').prop('disabled', true)
                    $('#node-input-jwtSecret').prop('disabled', true)
                    $('#node-input-tokenAutoExtend').prop('disabled', true)
                }
            }) // -- end of security change -- //

            // What mode is Node-RED running in? development or something else?
            $('#nrMode').text(RED.settings.uibuilderNodeEnv)

            //#endregion ---- security settings ---- //

            // Show/Hide the advanced settings
            $('#show-adv-props').css( 'cursor', 'pointer' )
            $('#show-adv-props').click(function(_e) {
                $('#adv-props').toggle()
                if ( $('#adv-props').is(':visible') ) {
                    $('#show-adv-props').html('<i class="fa fa-caret-down"></i> Advanced Settings')
                } else {
                    $('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                }
            })

            //#region ---- File Editor ---- //
            // Mark edit save/reset buttons as disabled by default
            fileIsClean(true)

            // Show the edit section, hide the main, adv & sec sections
            $('#show-edit-props').click(function(e) {
                e.preventDefault() // don't trigger normal click event
                $('#main-props').hide()
                $('#sec-props').hide()
                $('#show-sec-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                $('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                $('#show-sec-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                $('#edit-props').show()

                // Build the file list
                getFileList()

                if ( uiace.editorLoaded !== true ) {
                    // Clear out the editor
                        // @ts-ignore ts(2367)
                    if ($('#node-input-template').val('') !== '') $('#node-input-template').val('')

                    // Create the ACE editor component
                        //@ts-ignore Cannot find name 'RED'.ts(2304)
                    uiace.editor = RED.editor.createEditor({
                        id: 'node-input-template-editor',
                        mode: 'ace/mode/' + uiace.format,
                        value: that.template
                    })
                    // Keep a reference to the current editor session
                    uiace.editorSession = uiace.editor.getSession()
                    /** If the editor has changes, enable the save & reset buttons
                     * using input event instead of change since it's called with some timeout 
                     * which is needed by the undo (which takes some time to update)
                     **/
                    uiace.editor.on('input', function() {
                        // Is the editor clean?
                        fileIsClean(uiace.editorSession.getUndoManager().isClean())
                    })
                    /*uiace.editorSession.on('change', function(delta) {
                        // delta.start, delta.end, delta.lines, delta.action
                        console.log('ACE Editor CHANGE Event', delta)
                    }) */
                    uiace.editorLoaded = true

                    // Resize to max available height
                    setACEheight()
                    
                    // Be friendly and auto-load the initial file via the admin API
                    getFileContents()
                    fileIsClean(true)
                }
            })

            // Hide the edit section, show the main section
            $('#edit-close').click(function(e) {
                e.preventDefault() // don't trigger normal click event
                $('#main-props').show()
                $('#edit-props').hide()
            })

            // Handle the file editor change of folder/file (1st built on click of show edit button)
            $('#node-input-folder').change(function(_e) {
                // Rebuild the file list
                getFileList()
            })
            $('#node-input-filename').change(function(_e) {
                // Get the content of the file via the admin API
                getFileContents()
            })

            // Handle the folder new button
            $('#fldr-new-dialog_new').addClass('input-error').prop('disabled',true)
            $('#fldr-input-newname').addClass('input-error').on('input', function(){
                if ( $('#fldr-input-newname').val().length === 0) {
                    $('#fldr-input-newname').addClass('input-error')
                    $('#fldr').addClass('input-error').prop('disabled',true)
                } else {
                    $('#fldr-input-newname').removeClass('input-error')
                    $('#fldr-new-dialog_new').removeClass('input-error').prop('disabled',false)
                }
            })
            $('#fldr-new-dialog').dialog({ // define the dialog box
                autoOpen:false, modal:true, closeOnEscape:true,
                buttons: [
                    {
                        text: 'Create',
                        id: 'fldr-new-dialog_new',
                        click: function() {
                            // NB: Button is disabled unless name.length > 0 so don't need to check here
                            // Call the new file API
                            createNewFolder($('#fldr-input-newname').val())
                            $('#fldr-input-newname').val(null).addClass('input-error')
                            $('#fldr-new-dialog_new').addClass('input-error').prop('disabled',true)
                            $( this ).dialog( 'close' )
                        }
                    },{
                        text: 'Cancel',
                        click: function() {
                            $('#fldr-input-newname').val(null).addClass('input-error')
                            $('#fldr-new-dialog_new').addClass('input-error').prop('disabled',true)
                            $( this ).dialog( 'close' )
                        }
                    },
                ]
            }).keyup(function(e) { // make enter key fire the create
                if (e.keyCode == $.ui.keyCode.ENTER)
                    $('#fldr_new_dialog_new').click();
            })
            $('#btn-fldr-new').click(function(e) {
                $('#fldr_url').html( $('#node-input-url').val() )
                $('#fldr-new-dialog').dialog('open')  
            })
            // Handle the folder delete button
            $('#fldr-del-dialog').dialog({
                autoOpen:false, modal:true, closeOnEscape:true,
                buttons: [
                    {
                        text: 'Delete',
                        id: 'fldr-del-dialog_del',
                        click: function() {
                            // Call the delete folder API (uses the current folder)
                            deleteFolder()
                            $( this ).dialog( 'close' )
                        }
                    },{
                        text: 'Cancel',
                        click: function() {
                            $( this ).dialog( 'close' )
                        }
                    },
                ]
            })
            $('#btn-fldr-del').click(function(e) {
                $('#fldr-del-dialog_del').addClass('input-error').css('color','brown')
                $('#fldr-del-name').text($('#node-input-folder').val())
                if ( $('#node-input-folder').val() === 'src' ) {
                    if ( $('#node-input-copyIndex').is(':checked') ) {
                        $('#fldr-del-recopy').css('color','').text('Copy flag is set so the src folder will be recreated and the index.(html|js|css) files will be recopied from the master template.')
                    } else {
                        $('#fldr-del-recopy').css('color','brown').text('Copy flag is NOT set so the src folder will NOT be recopied from the master template.')
                    }    
                } else {
                    $('#fldr-del-recopy').css('color','').text('')
                }
                $('#fldr-del-dialog').dialog('open')                  
            })

            // Handle the file editor reset button (reload the file)
            $('#edit-reset').click(function(e) {
                e.preventDefault() // don't trigger normal click event

                // Get the content of the file via the admin API
                getFileContents()
                $('#file-action-message').text('')
            })
            // Handle the file editor save button
            $('#edit-save').click(function(e) {
                e.preventDefault() // don't trigger normal click event

                var authTokens = RED.settings.get('auth-tokens')
                
                // Post the updated content of the file via the admin API
                // NOTE: Cannot use jQuery POST function as it sets headers that trigger a CORS error. Do it using native requests only.
                // Clients will be reloaded if the reload checkbox is set.
                var request = new XMLHttpRequest()
                var params = 'fname=' + $('#node-input-filename').val() + '&folder=' + $('#node-input-folder').val() + 
                    '&url=' + $('#node-input-url').val() + 
                    '&reload=' + $('#node-input-reload').val() + 
                    '&data=' + encodeURIComponent(uiace.editorSession.getValue())
                request.open('POST', 'uibputfile', true)
                request.onreadystatechange = function() {
                    if (this.readyState === XMLHttpRequest.DONE) {
                        if (this.status === 200) {
                            // Request successful
                            // display msg - blank msg when new edits present
                            $('#file-action-message').text('File Saved')
                            fileIsClean(true)
                        } else {
                            // Request failed
                            // display msg - blank msg when new edits present
                            $('#file-action-message').text('File Save FAILED')
                        }
                    }
                }
                request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
                // If admin ui is protected with a login, we need to send the access token
                if (authTokens) request.setRequestHeader('Authorization', 'Bearer ' + authTokens.access_token)
                request.send(params)

            })

            //#region Handle the Delete file button
            $('#edit_delete_dialog').dialog({
                autoOpen:false, modal:true, closeOnEscape:true,
                buttons: [
                    {
                        text: 'Delete',
                        id: 'edit_del_dialog_del',
                        click: function() {
                            // Call the delete file API (uses the current file)
                            deleteFile()
                            $( this ).dialog( 'close' )
                        }
                    },{
                        text: 'Cancel',
                        click: function() {
                            $( this ).dialog( 'close' )
                        }
                    },
                ]
            })
            $('#edit-delete').click(function(_e) {
                $('#edit_del_dialog_del').addClass('input-error').css('color','brown')
                $('#edit-delete-name').text($('#node-input-folder').val() + '/' + $('#node-input-filename').val())
                if ( $('#node-input-folder').val() === 'src' ) {
                    if ( $('#node-input-copyIndex').is(':checked') ) {
                        $('#edit-delete-recopy').css('color','').text('Copy flag is set so index.(html|js|css) files will be recopied from master template.')
                    } else {
                        $('#edit-delete-recopy').css('color','brown').text('Copy flag is NOT set so index.(html|js|css) files will NOT be recopied from master template.')
                    }    
                } else {
                    $('#edit-delete-recopy').css('color','').text('')
                }
                $('#edit_delete_dialog').dialog('open')  
            })
            //#endregion

            //#region Handle the New file button
            $('#edit_new_dialog_new').addClass('input-error').prop('disabled',true)
            $('#edit-input-newname').addClass('input-error').on('input', function(){
                if ( $('#edit-input-newname').val().length === 0) {
                    $('#edit-input-newname').addClass('input-error')
                    $('#edit_new_dialog_new').addClass('input-error').prop('disabled',true)
                } else {
                    $('#edit-input-newname').removeClass('input-error')
                    $('#edit_new_dialog_new').removeClass('input-error').prop('disabled',false)
                }
            })
            $('#edit_new_dialog').dialog({
                autoOpen:false, modal:true, closeOnEscape:true,
                buttons: [
                    {
                        text: 'Create',
                        id: 'edit_new_dialog_new',
                        click: function() {
                            // NB: Button is disabled unless name.length > 0 so don't need to check here
                            // Call the new file API
                            createNewFile($('#edit-input-newname').val())
                            $('#edit-input-newname').val(null).addClass('input-error')
                            $('#edit_new_dialog_new').addClass('input-error').prop('disabled',true)
                            $( this ).dialog( 'close' )
                        }
                    },{
                        text: 'Cancel',
                        click: function() {
                            $('#edit-input-newname').val(null).addClass('input-error')
                            $('#edit_new_dialog_new').addClass('input-error').prop('disabled',true)
                            $( this ).dialog( 'close' )
                        }
                    },
                ]
            }).keyup(function(e) { // make enter key fire the create
                if (e.keyCode == $.ui.keyCode.ENTER)
                    $('#edit_new_dialog_new').click();
            })
            $('#edit-new').click(function(_e) {
                $('#file_url').html( $('#node-input-url').val() )
                $('#file_fldr').html( $('#node-input-folder').val() )
                $('#edit_new_dialog').dialog('open')  
            })
            //#endregion

            // Handle the expander button (show full screen editor) - from core function node
            $('#node-function-expand-js').click(function(e) {
                e.preventDefault()

                // Creates another edit session with max width
                var value = uiace.editor.getValue();
                RED.editor.editText({
                    mode: $("#node-input-format").val(), //mode: $("#node-input-format").val(),
                    value: value,
                    width: "Infinity",
                    cursor: uiace.editor.getCursorPosition(),
                    complete: function(v,cursor) {
                        // v contains the returned text
                        uiace.editor.setValue(v, -1);
                        uiace.editor.gotoLine(cursor.row+1,cursor.column,false);
                        setTimeout(function() {
                            uiace.editor.focus();
                            // Check if anything changed
                            if ( v !== value ) {
                                fileIsClean(false)
                            }
                        },300);
                    }
                })

                // Are we in fullscreen? Variable is updated when oneditresize calls setACEheight()
                // if (uiace.fullscreen === false) {
                //     // Select the editor components and make full-screen, triggers oneditresize()
                //     var viewer = $('#edit-props')[0]
                //     var rFS = viewer.requestFullscreen || viewer.mozRequestFullScreen || viewer.webkitRequestFullscreen || viewer.msRequestFullScreen
                //     if (rFS) rFS.call(viewer)
                    
                //     // TODO: Add popup if no method is available
                // } else {
                //     // Already in fullscreen so lets exit, triggers oneditresize()
                //     document.exitFullscreen()
                // }

            })
            //#endregion ---- File Editor ---- //

            //#region ---- npm ---- //
            // NB: Assuming that the edit section is closed
            // Show the npm section, hide the main, adv & security sections
            $('#show-npm-props').click(function(e) {
                e.preventDefault() // don't trigger normal click event
                $('#main-props').hide()
                $('#adv-props').hide()
                $('#sec-props').hide()
                $('#show-adv-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                $('#show-sec-props').html('<i class="fa fa-caret-right"></i> Advanced Settings')
                $('#npm-props').show()

                /** todo! Improve feedback */
                //#region Setup the package list
                $('#node-input-packageList').editableList({
                    addItem: addPackageRow, // function
                    removeItem: removePackageRow, // function(data){},
                    resizeItem: function(_row,_index) {},
                    header: $('<div>').append('<h4 style="display: inline-grid">Installed Packages</h4>'),
                    height: 'auto',
                    addButton: true,
                    removable: true,
                    scrollOnAdd: true,
                    sortable: false,
                })

                // reset and populate the list
                $('#node-input-packageList').editableList('empty')
                packages.forEach( packageName => {
                    if ( packageName !== 'socket.io' ) // ignore socket.io
                        $('#node-input-packageList').editableList('addItem',packageName)
                })


                // spinner
                $('.red-ui-editableList-addButton').after(' <i class="spinner"></i>')
                $('i.spinner').hide()
                //#endregion --- package list ---- //

            })
            // Hide the npm section, show the main section
            $('#npm-close').click(function(e) {
                e.preventDefault() // don't trigger normal click event

                $('#main-props').show()
                $('#npm-props').hide()
            })
            //#endregion ---- npm ---- //

            /** TODO: Get list of installed ACE themes, add chooser, save current choice
             * that.editor.setTheme("ace/theme/monokai")
             * uiace.config.set("basePath", "https://url.to.a/folder/that/contains-ace-modes");
             * uiace.config.setModuleUrl("ace/theme/textmate", "url for textmate.js");
             **/

        }, // ---- End of oneditprepare ---- //

        /** Runs before save */
        oneditsave: function() {
            // xfer the editor text back to the template var
            //$('#node-input-template').val(this.editor.getValue())
            // Get rid of the editor
            if ( uiace.editorLoaded === true ) {
                uiace.editor.destroy()
                delete uiace.editor
                uiace.editorLoaded = false
            }

            // Check for url rename - note that validation of new url is done in the validate function
            if ( $('#node-input-url').val() !== this.url ) {
                this.oldUrl = this.url
            } else {
                if ( this.oldUrl !== undefined ) this.oldUrl = undefined
            }

        }, // ---- End of oneditsave ---- //

        /** Runs before cancel */
        oneditcancel: function() {
            // Get rid of the editor
            if ( uiace.editorLoaded === true ) {
                uiace.editor.destroy()
                delete uiace.editor
                uiace.editorLoaded = false
            }
        }, // ---- End of oneditcancel ---- //

        /** Handle window resizing for the editor */
        oneditresize: function(_size) {

            setACEheight()

        }, // ---- End of oneditcancel ---- //

        oneditdelete: function() {
            const that = this
            let myNotify = RED.notify(`<b>DELETE</b>: <p>You are deleting a uibuilder instance with url <i>${this.url}</i>.<br>Would you like to also delete the folder?<br><b>WARNING</b>: This cannot be undone.</p>`, {
                modal: true,
                fixed: true,
                type: 'warning', // 'compact', 'success', 'warning', 'error'
                buttons: [
                    {
                        text: "Yes",
                        click: function(e) {
                            // Mark folder for Deletion (actually done by the js file in `node.on('close', ..)`, handed off to uiblib:processClose)
                            $.ajax({
                                type: 'PUT',
                                dataType: 'json',
                                url: './uibuilder/admin/' + that.url,
                                data: {
                                    'cmd': 'deleteondelete',
                                },
                            })
                                .fail(function(jqXHR, textStatus, errorThrown) {
                                    console.error( '[uibuilder:oneditdelete:PUT] Error ' + textStatus, errorThrown )
                                    RED.notify(`uibuilder: Request url folder delete - error.<br>Folder will not be deleted, please delete manually.<br>${errorThrown}`, {type:'error'})
                                })

                            myNotify.close()
                            RED.notify(`The folder <i>${that.url}</i> will be deleted when you redeploy.`, {type:'compact'})
                        }
                    },
                    {
                        text: "NO",
                        class:"primary",
                        click: function(e) {
                            myNotify.close()
                        }
                    }
                ]
        
            })
        },

    }) // ---- End of registerType() ---- //

})()
</script>

<script type="text/html" data-template-name="uibuilder">
    <div id="main-props" aria-label="Hideable main properties section">
        <div aria-label="Node Name" class="form-row" title="Node Name (descriptive only)">
            <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
            <input type="text" id="node-input-name">
        </div>

        <div aria-label="Topic" class="form-row" title="Adds a msg.topic to input msgs if not already provided">
            <label for="node-input-topic"><i class="fa fa-tasks"></i> Topic</label>
            <input type="text" id="node-input-topic">
        </div>

        <div aria-label="Home 'page' id. Defines the URL used" class="form-row" title="Make sure this gives a unique URL. Also used as a folder name for resources.">
            <label for="node-input-url"><i class="fa fa-globe"></i> URL</label>
            <input type="text" id="node-input-url" title="required, <20 chars, unique, cannot be 'templates'">
        </div>

        <div aria-label="Buttons to show 'edit files' & 'mng front-end libs', hide this section" class="form-row">
            <button class="btn" id="show-edit-props" title="Edit the front-end code">
                Edit Files
            </button>
            <button class="btn" id="show-npm-props" title="Install/remove/update npm packages to use with your UI">
                Manage front-end libraries
            </button>
        </div>

        <div aria-label="Show template selection, collapsible" class="form-row">
            <b id="show-templ-props" title="Show&sol;Hide the template selection settings">
                <i class="fa fa-caret-right"></i> Template Settings
            </b>
            <div id="adv-templ" aria-label="Hideable Template Settings Section" style="display:hidden;border:1px solid silver;padding:.5em;">
                <div aria-label="How to use the template selector">
                    <p>
                        By default, uibuilder creates a template folder with a minimal template.
                        If you wish to use a different template, select and load it here.
                    </p>
                    <p>
                        Selecting a Template from the drop-down <b>does not</b> load the template
                    </p>
                    <p>
                        Loading a Template <b>will overwrite</b> existing files.
                    </p>
                </div>

                <label for="node-input-templateFolder" style="width:7em;"><i class="fa fa-window-restore"></i> Template:</label>
                <select id="node-input-templateFolder" style="width:73%;"></select>

                <div aria-label="Hidden unless external template is selected above" id="et-input" class="form-row" title="Input a Degit compatible repository name. e.g. totallyinformation/uib-template-test" style="display:none">
                    <label for="node-input-extTemplate"><i class="fa fa-globe"></i> Ext. Template</label>
                    <input type="text" id="node-input-extTemplate" placeholder="totallyinformation/uib-template-test">
                </div>

                <button class="btn" id="btn-load-template" style="margin-top: 0.3em;" title="Load selected template, overwriting existing files">
                    Load & Overwrite
                </button>
            </div>
        </div>

        <div aria-label="Show advanced edit properties, collapsible" class="form-row">
            <b id="show-adv-props" title="Show&sol;Hide the advanced configuration settings">
                <i class="fa fa-caret-right"></i> Advanced Settings
            </b>
            <div id="adv-props" aria-label="Hideable Advanced Settings Section" style="display:hidden;border:1px solid silver;padding:.5em;">
                <!-- How long do we want resources to be cached for? *** NOT READY *** 
                <div class="form-row" title="Cache max. age in seconds (blank or zero for no caching)">
                    <label for="node-input-maxAge"><i class="fa fa-clock-o"></i> Cache Age</label>
                    <input type="number" step="1" pattern="\d+" min="0" max="31536000" id="node-input-maxAge" style="width: 10em;"> Seconds
                </div> -->
                
                <div aria-label="Select source folder" class="form-row">
                    <label for="node-input-sourceFolder" style="width:7em;"><i class="fa fa-folder"></i> Template:</label>
                    <select id="node-input-sourceFolder" style="width:73%;">
                        <option value="src">src</option>
                        <option value="dist">dist</option>
                    </select>
                </div>

                <!-- Provide a searchable file index view? -->
                <div class="form-row" title="If turned on, an index page showing all available files is created. This can be a security vulnerability and not recommended in Production.">
                    <input type="checkbox" id="node-input-showfolder" style="display: inline-block; width: auto; vertical-align: top;">
                    <label for="node-input-showfolder" style="width: 90%;">Show web view of source files at <span id="node-input-showfolder-url">_</span>.</label>
                </div>

                <!-- Allow scripts/styles to be passed to front-end? -->
                <div class="form-row" title="Add msg.script&sol;msg.style to the input msg. This can be a security issue as the data is NOT VETTED.">
                    Allow passing to the front-end: <br>
                    <input type="checkbox" id="node-input-allowScripts" style="display: inline-block; width: auto; vertical-align: top;">
                    <label for="node-input-allowScripts">Scripts? (JS)</label>
                    <input type="checkbox" id="node-input-allowStyles" style="display: inline-block; width: auto; vertical-align: top;">
                    <label for="node-input-allowStyles">Styles? (CSS)</label>
                </div>

                <div class="form-tips node-help" title="">
                    Warning: Most people will not need the following.
                </div>

                <div aria-label="Forward flag" class="form-row" title="forward inbound msg's direct to the output">
                    <!-- Forward input msgs to output -->
                    <input type="checkbox" id="node-input-fwdInMessages" style="display: inline-block; width: auto; vertical-align: top;">
                    <label for="node-input-fwdInMessages" style="width: 40%;">
                        Forward input to output?
                    </label>
                </div>
            </div>
        </div>

        <div aria-label="Show security properties, collapsible" class="form-row">
            <b id="show-security-props" title="Show&sol;Hide security settings">
                <i class="fa fa-caret-right"></i> Security Settings
            </b>
            <!-- ---- Hideable Security Settings Section ---- -->
            <div id="sec-props" style="display:hidden;border:1px solid silver;padding:.5em;">
                <!-- Do we want security turned on? -->
                <div class="form-row">
                    <input type="checkbox" id="node-input-useSecurity" style="display: inline-block; width: auto; vertical-align: top;">
                    <label for="node-input-useSecurity" style="width: 90%;">Use uibuilder's security?</label>
                </div>

                <!-- How long do we want our sessions to be? -->
                <div class="form-row" title="Required. Session length. If client doesn't send a msg in this time, it is logged out.">
                    <label for="node-input-sessionLength"><i class="fa fa-clock-o"></i> Sess Length</label>
                    <input type="number" step="1" pattern="\d+" min="0" max="31536000" id="node-input-sessionLength" style="width: 10em;"> Seconds
                </div>

                <!-- JWT Secret (credential) -->
                <div class="form-row" title="Required. A string to be used to sign&sol;validate JWT's. Stored as a credential so not copied when exporting a flow.">
                    <label for="node-input-jwtSecret"><i class="fa fa-user-secret"></i> JWT Secret</label>
                    <input type="text" id="node-input-jwtSecret">
                </div>
                
                <!-- Do we want to auto-extend the token life? -->
                <div class="form-row" title="While the client continues to send ping queries (~30sec), the expiry will be moved fwd 30 sec">
                    <input type="checkbox" id="node-input-tokenAutoExtend" style="display: inline-block; width: auto; vertical-align: top;">
                    <label for="node-input-tokenAutoExtend" style="width: 90%;">Auto-extend the token expiry?</label>
                </div>

                <div class="form-row">
                    Node-RED is running in <i><b><span id="nrMode"></span></b></i> mode.
                </div>

                <div class="form-tips node-help" title="">
                    Warning: To safely deploy security, you really need to be using TLS.
                    If you deploy without TLS, you will get warnings. If you try to deploy to production without TLS,
                    security will not turn on.
                </div>
                <div class="form-tips node-help" title="">
                    In development mode, if the "Copy Index" (see advanced settings) flag is on, a simplistic template 
                    <code>security.js</code> file will be copied to <code>&lt;uibRoot>/.config/security.js</code>.
                    This should be adapted for your own needs and contains the code to validate users and handle sessions.
                </div>
            </div>
        </div>
    </div>

    <!-- #region ---- Hideable File Editor Section ---- -->
    <div id="edit-props">
        <!-- Drop-down for selecting folder & new/del buttons -->
        <div class="form-row" title="Choose between the src, dist (built src) or root folders">
            <label for="node-input-folder" style="width:5em;">
                <i class="fa fa-folder-open-o"></i> Folder:
            </label>
            <select id="node-input-folder" style="width:63%;"></select>
            <span id="fldr-buttons">
                <button class="btn" id="btn-fldr-new" title="Add a new folder">New</button>
                <div id="fldr-new-dialog" title="Create a new folder" dialog>
                    <label for="fldr-input-newname">New Folder Name</label>
                    <input type="text" id="fldr-input-newname">
                    <div title="" style="margin-top:.5em;">
                        New folder will be created inside 
                        <code>&lt;uibRoot>/<span id="fldr_url">&lt;url></span></code>.<br>
                        Do not forget to include sub-folder name if needed.<br>
                        e.g. <code>src/mynewfldr</code>
                    </div>
                </div>                  
                <button class="btn" id="btn-fldr-del" title="Delete the selected folder">Del</button>
                <div id="fldr-del-dialog" title="Delete the currently selected folder" dialog>
                    <b>Folder to be deleted</b>
                    <p><code id="fldr-del-name"></code></p>
                    <p id="fldr-del-recopy"></p>
                    <p style="border:2px solid brown;color:brown;font-weight:bold;padding:5px 5px;">
                        Warning: This action cannot be undone.<br />
                        If you delete the <code>src</code> or <code>dist</code> folders
                        some things might not work correctly.
                    </p>
                </div>                  
            </span>
        </div>

        <!-- Drop-down for selecting file to edit -->
        <div class="form-row">
            <label for="node-input-filename"  style="width:5em;"><i class="fa fa-file-code-o"></i> File:</label>
            <select id="node-input-filename" style="width: 83%;"></select>
        </div>

        <!-- Save/Reset/close/(message)/Delete -->
        <div class="form-row">
            <button class="btn" id="edit-save" title="Save any changes to the file">Save</button>
            <button class="btn" id="edit-reset" title="Reset any changes to last saved version (cancel changes)">Reset</button>
            <button class="btn" id="edit-close" title="Close the edit window (lose any unsaved changes)">Close</button>

            <span id="file-action-message"></span>

            <div style="position: absolute;right:1.6em;display:inline-block;">
                <button class="btn" id="edit-new" title="Add a new file.">
                    New
                </button>
                <div dialog id="edit_new_dialog" title="Create a new file">
                    <label for="edit-input-newname">New File Name</label>
                    <input type="text" id="edit-input-newname">
                    <div style="margin-top:.5em;">
                        New file will be created inside the 
                        <code>
                            &lt;uibRoot>/<span id="file_url">&lt;url></span>/<span id="file_fldr">&lt;folder></span>
                        </code> folder.<br>
                        You can include a sub-folder name if needed. Intermediate folders will be automatically created.<br>
                        e.g. <code>src/mynewfldr/myfile.js</code>
                    </div>
                </div>                  
                <button class="btn" id="edit-delete" title="Delete the file. Will reset to the default template if Copy from templates flag is set.">
                    Delete
                </button>
                <div id="edit_delete_dialog" title="Delete the currently selected file" dialog>
                    <b>File to be deleted</b>
                    <p><code id="edit-delete-name"></code></p>
                    <p id="edit-delete-recopy"></p>
                    <p style="border:2px solid brown;color:brown;font-weight:bold;padding:5px 5px;">Warning: This action cannot be undone.</p>
                </div>                  
            </div>
        </div>

        <!-- Edit box  editor-tray-content -->
        <div id="edit-outer" class="form-row node-text-editor-row" style="position:relative">
            <div style="height: 350px; min-height:150px;" class="node-text-editor" id="node-input-template-editor" ></div>
            <div style="height: 350px; min-height:150px;" style="display:hidden" id="node-input-template-editor-no-file" >
                No file available, please select a different file or folder.
            </div>
        </div>

        <!-- file data (hidden), language selector -->
        <div class="form-row">
            <button id="node-function-expand-js" class="editor-button"><i class="fa fa-expand"></i></button>

            <!-- Option to reload any connected client pages after edit-save -->
            <input type="checkbox" id="node-input-reload" style="width:auto;"
                   title="If set, all connected clients will reload the page they are on when any change is saved."
                >
            <label for="node-input-reload" style="width:80%;">Reload connected clients on save?.</label>

            <input type="hidden" id="node-input-template" data-isClean="true">
            <div style="position: absolute; right:1.6em;display:inline-block; text-align: right;">
                <select id="node-input-format" style="width:110px; padding:0;">
                    <option value="text">Text</option>
                    <option value="html">HTML</option>
                    <option value="javascript">Javascript</option>
                    <option value="json">JSON</option>
                    <option value="css">CSS</option>
                    <option value="scss">SCSS</option>
                    <option value="handlebars">mustache</option>
                    <option value="markdown">Markdown</option>
                    <option value="yaml">YAML</option>
                </select>
            </div>
        </div>
    </div>
    <!-- #endregion -->

    <!-- #region ---- Hideable NPM Section ---- -->
    <div id="npm-props">
        <b>Front-End Library Manager</b>
        <p>
            Install, remove or update npm packages that provide front-end libraries such as
            VueJS, jQuery, MoonJS, etc.
        </p>
        <p>
            You can search for packages on the 
            <a href="https://www.npmjs.com/" target="_blank" class="red-ui-button editor-button">official npm site</a> 
            or on <a href="https://npms.io/" target="_blank" class="red-ui-button editor-button">npms.io</a>.<br>
            Copy the name, click on "+ add" below and paste in the input.
        </p>
        <!-- Package List -->
        <div class="form-row">
            <ol id="node-input-packageList"></ol>
        </div>
        <!-- Close Button -->
        <div class="form-row">
            <button class="btn" id="npm-close" title="Close the library"">Close Library Manager</button>
        </div>
    </div>
    <!-- #endregion -->

    <!-- #region Info -->
    <div id="info">
        <a id="uibuilderurl" title="Open the uibuilder web page" href="uibindex" target="_blank" class="red-ui-button editor-button"></a> 
        <a href="uibindex" target="_blank" class="red-ui-button editor-button" title="Shows available libraries, configuration and other detailed uibuilder information">uibuilder details</a>
        <a id="uibinstanceconf" href="./uibuilder/instance/_url_?cmd=showinstancesettings" target="_blank" class="red-ui-button editor-button" title="Shows detailed configuration for this instance of uibuilder">Instance Details</a>
        <a href="./uibuilder/techdocs" target="_blank" class="red-ui-button editor-button" title="Shows the uibuilder technical documentation as a new page">Tech Docs</a>
    </div>
    <div class="form-row" id="info-webserver"></div>
    <!-- #endregion -->

</script>

<script type="text/html" data-help-name="uibuilder">
    <p>Easily create a data-driven web user interface (UI)</p>
    <p>
        Create dynamic, data-driven user interfaces that send and receive messages to/from Node-RED.
        Creates a file/folder structure that is used to deliver static resources (html, css, js, images, etc) and
        manages the delivery of vendor library resources (VueJS, jQuery, etc).
        You can also pass script and style information to the front-end in a message.
    </p>

    <p id="uib_help_docs">
        Detailed technical documentation can be found <a href="./uibuilder/techdocs" target="_blank">HERE</a>.
    </p>

    <p>
        Specific information on how to use the front-end library: <a href="./uibuilder/techdocs/#/front-end-library" target="_blank">uibuilderfe</a>
    </p>

    <p>
        Still more information is available in the <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/" target="_blank">uibuilder WIKI</a>.
    </p>

    <h3>Input msg</h3>
    <dl class="message-properties">
        <dt class="optional">payload <span class="property-type">(string | buffer)</span></dt>
        <dd> Optionally, the payload of the message to send to all connected client browser tabs. </dd>
        <dt class="optional">topic <span class="property-type">(string)</span></dt>
        <dd> Optionally, the MQTT topic to use. Takes preference over the topic defined in settings.</dd>

        <dt class="optional">script <span class="property-type">(string | string[])</span></dt>
        <dd>
            Optionally, a string or array of strings containing valid JavaScript.
            This will be added to the web page dynamically. Currently contains minimal validation so care is required.
            This feature is <b>off</b> by default, turn on in the Advanced Settings.
        </dd>
        <dt class="optional">style <span class="property-type">(string | string[])</span></dt>
        <dd>
            Optionally, a string or array of strings containing valid CSS for styling the front-end page.
            This will be added to the web page dynamically. Currently contains minimal validation so care is required.
            This feature is <b>off</b> by default, turn on in the Advanced Settings.
        </dd>
    </dl>

    <h3>Output msgs</h3>
    <ol class="node-ports">
        <li>Standard output msg
            <dl class="message-properties">
                <dt>payload <span class="property-type">(string | buffer)</span></dt>
                <dd>
                    A copy of any inbound payload unless altered by the front-end page.
                </dd>
                <dt>topic <span class="property-type">(string)</span></dt>
                <dd>
                    A copy of any inbound topic if present. Otherwise, the topic from the node's settings. Could be changed by the front-end
                    page but it really isn't recommended.
                </dd>
                <dt>_socketId <span class="property-type">(string)</span></dt>
                <dd>
                    Identifies the sending browser/tab. Retain this if needing to send a reply back to the originator.
                    Remove if needing to send a reply to all connected broweser/tabs.
                    Note that the initial part of the ID contains the source URI.
                </dd>
                <dt>other</dt>
                <dd>
                    Note that any inbound msg.script or msg.style is stripped and not sent to the output.
                </dd>
            </dl>
        </li>
        <li>Control output msg
            <dl class="message-properties">
                <dt>uibuilderCtrl <span class="property-type">(string)</span></dt>
                <dd>
                    The name of the control message. See the
                    <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Control-Message-Structure" target="_blank">WIKI page</a>
                    for details.
                </dd>
                <dt>cache-control <span class="property-type">(string)</span></dt>
                <dd>
                    "REPLAY". A command for the companion app
                    <a href="https://github.com/TotallyInformation/node-red-contrib-infocache" target="_blank">node-red-contrib-infocache</a>.
                    See the
                    <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Message-Caching" target="_blank">WIKI</a>
                    for details.
                </dd>
                <dt>from <span class="property-type">(string)</span></dt>
                <dd>
                    Identifies the source of the msg since both <i>server</i> (back-end) and <i>client</i> (front-end) control messages
                    will appear out of port #2.
                </dd>
                <dt>_socketId <span class="property-type">(string)</span></dt>
                <dd>
                    Identifies the sending browser/tab. Retain this if needing to send a reply back to the originator.
                    Remove if needing to send a reply to all connected broweser/tabs.
                    Note that the initial part of the ID contains the source URI.
                </dd>
            </dl>
        </li>
    </ol>

    <h3>Node Settings</h3>
        <dl class="message-properties">
            <dt>Name <span class="property-type">(string) node-input-name</span></dt>
            <dd>A short description shown in the admin interface</dd>
            <dt>Topic <span class="property-type">(string) node-input-topic</span></dt>
            <dd>A topic name to use if the incoming msg does not contain one.</dd>
            <dt>URL <span class="property-type">(string) node-input-url</span></dt>
            <dd>
                The URL path that the resulting page and other resources will be served from.
                Must be unique across Node-RED. Will also use the Node-RED scheme (http/https)
                and port. Will also be prefixed by the Node-RED setting <i>httpNodeRoot</i>.<br>
                The default URL is <a href="/uibuilder" target="_blank">/uibuilder</a>.<br>
                The url is also used to define the server filing system path for front-end code files.<br>
                The url must be valid as a filename as well as a url path. It may not exceed 20 characters.
            </dd>

            <dt>Edit Source Files <span class="property-type">(button)</span></dt>
            <dd>
                Clicking this button will hide the above settings and display a file editor allowing you
                to edit the front-end files that define your user interface.<br />
                All text-based files in the `&lt;userDir>/uibuilder/&lt;url>/src` folder on the server's filing system will
                be available to edit.<br />
                To return back, click the red <kbd class="help-key">Done</kbd> or grey <kbd class="help-key">Cancel</kbd> buttons.
                <p>
                    Note that you can expand the edit panel by clicking on the <kbd class="help-key">&#x2921;</kbd> arrow button underneath the panel.
                    To exit fullscreen, press the same button (now highlighted) or the <kbd class="help-key">Esc</kbd> key.
                </p>
            </dd>
            <dt>Manage Front-End Libraries <span class="property-type">(button)</span></dt>
            <dd>
                Clicking this button will hide the above settings and display a list of currently available
                front-end library packages. This interface allows you to add, remove and update any library
                that can be installed via <a href="https://www.npmjs.com/" target="_blank">npm</a>.
            </dd>
        </dl>
        <h4>Advanced Settings</h4>
        <dl class="message-properties">
            <dt>Allow scripts to be passed to front-end? <span class="property-type">(boolean) allowScripts</span></dt>
            <dd>
                If on, any string(s) in <code>msg.script</code> will be dynamically added to the scripts of the web page.
                Turn off to prevent scripts from being dynamically added and executed.
            </dd>
            <dt>Allow styles to be passed to front-end? <span class="property-type">(boolean) allowStyles</span></dt>
            <dd>
                If on, any string(s) in <code>msg.style</code> will be dynamically added to the styles of the web page.
                Turn off to prevent styles from being dynamically added.
            </dd>

            <dt>Copy index.(css|js) from templates to this instance if they don't exist?
                <span class="property-type">(boolean) node-input-copyIndex</span>
            </dt>
            <dd>
                If on (default), index.(css|html|js) files are copied from the master template folder
                if they don't exist in the local assets folder <code>~/.node-red/uibuilder/{url}/src/</code>.
                Turn off if you don't want this to happen (e.g. if you prefer them in a sub-folder).
            </dd>

            <dt>Show web view of source files at ... <span class="property-type">(boolean) showfolder</span></dt>
            <dd>
                If on, an extra web page is dynamically created at the indicated URL. That page shows an index listing
                of all files in the instances <i>src</i> folder. As this could be a security issue, it is turned off by default.
            </dd>

            <dt>Forward received messages direct to output? <span class="property-type">(boolean) node-input-fwdInMessages</span></dt>
            <dd>
                Forward's a copy of every received message direct to the output.
                Adds the topic from the above setting if one isn't present in the msg.
                <p>
                    <i>Note</i> that this may result in multiple output messages if your front-end
                    code also auto-sends inbound messages.
                </p>
            </dd>
        </dl>

        <h4>Path & Module Info</h4>
        <dl class="message-properties">
            <dt>Web page URL <span class="property-type">(link)</span></dt>
            <dd>
                The web app url for this instance. Click to see the app page.
            </dd>
            <dt>Detailed Information <span class="property-type">(link)</span></dt>
            <dd>
                Shows a web page with detailed information about the overall uibuilder
                configuration. Including details of all of the vendor modules that are
                available and all of the uibuilder endpoints.
            </dd>
        </dl>

    <h3>File/Folder Structure</h3>
    <p>
        For more information, see the GitHub page for <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder" target="_blank">node-red-contrib-uibuilder</a>
    </p>


    <h3>Details</h3>
    <p>
        For more information, see the GitHub page for <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder" target="_blank">node-red-contrib-uibuilder</a>
    </p>
    <p>
        There are also examples of how to use uibuilder in the <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki" target="_blank">GitHub WIKI</a>.
    </p>
    <p>
        uibuilder can be discussed in the <a href="https://discourse.nodered.org/" target="_blank">Node-RED Forum</a> and in the <a href="https://node-red.slack.com/messages/C7K77MG06" target="_blank">Node-RED #uibuilder Slack channel</a>. Issues/bugs can be raised in <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues" target="_blank">GitHub</a>.
    </p>

    <h3>Admin Index (Details) page</h3>
    <p>
        The <a href="uibindex" target="_blank">&lt;admin url&gt;/uibindex</a> page lists all of the uibuilder endpoints and other details. You can use the following parameters:
    </p>
    <dl class="message-properties">
        <dt>type <span class="property-type">(string)</span></dt>
        <dd>
            The type of the data returned.
            <dl class="message-properties">
                <dt>html <span class="property-type">(default)</span></dt>
                <dd>
                    Also used if no type parameter is given. 
                    Returns an HTML page showing the details for all deployed uibuilder nodes.
                    Shows the server file locations of the instance and for vendor libraries.
                </dd>
                <dt>json</dt>
                <dd>
                    Returns JSON data with both the source node ID's and matching URL's.
                </dd>
                <dt>urls</dt>
                <dd>
                    Returns a JSON array of just the URL's in use from deployed uibuilder nodes.
                    Used internally to ensure that new nodes use unique url's.
                </dd>
            </dl>
        </dd>

        <dt>check <span class="property-type">(string)</span></dt>
        <dd>
            Passing a uibuilder instance name to the check parameter will result in a boolean (true/false)
            return. True if it is being delivered by a uibuilder instance,
            false otherwise. e.g. <code>?check=uibuilder</code>
        </dd>
    </dl>
    <ul>
    </ul>

</script>
